/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2005-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.feature.visitor;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.geotools.api.feature.simple.SimpleFeature;
import org.geotools.api.feature.simple.SimpleFeatureType;
import org.geotools.api.filter.FilterFactory;
import org.geotools.api.filter.expression.Expression;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.filter.IllegalFilterException;

/**
 * Calculates the maximum value of an attribute.
 *
 * @author Cory Horner, Refractions Research Inc.
 * @since 2.2.M2
 */
public class MaxVisitor implements FeatureCalc, FeatureAttributeVisitor {
    private Expression expr;
    Comparable maxvalue;
    boolean visited = false;
    int countNull = 0;
    int countNaN = 0;

    public MaxVisitor(String attributeTypeName) {
        FilterFactory factory = CommonFactoryFinder.getFilterFactory(null);
        expr = factory.property(attributeTypeName);
    }

    public MaxVisitor(int attributeTypeIndex, SimpleFeatureType type)
            throws IllegalFilterException {
        FilterFactory factory = CommonFactoryFinder.getFilterFactory(null);
        expr = factory.property(type.getDescriptor(attributeTypeIndex).getLocalName());
    }

    public MaxVisitor(String attrName, SimpleFeatureType type) throws IllegalFilterException {
        FilterFactory factory = CommonFactoryFinder.getFilterFactory(null);
        expr = factory.property(type.getDescriptor(attrName).getLocalName());
    }

    public MaxVisitor(Expression expr) throws IllegalFilterException {
        this.expr = expr;
    }

    public void init(SimpleFeatureCollection collection) {
        // do nothing
    }

    @Override
    public List<Expression> getExpressions() {
        return Arrays.asList(expr);
    }

    @Override
    public Optional<List<Class>> getResultType(List<Class> inputTypes) {
        return CalcUtil.reflectInputTypes(1, inputTypes);
    }

    /**
     * Visitor function, which looks at each feature and finds the maximum.
     *
     * @param feature the feature to be visited
     */
    public void visit(SimpleFeature feature) {
        visit((org.geotools.api.feature.Feature) feature);
    }

    @Override
    public void visit(org.geotools.api.feature.Feature feature) {
        Object attribValue = expr.evaluate(feature);

        if (attribValue == null) {
            countNull++; // increment the null count, but don't store its value
            return;
        }

        if (attribValue instanceof Double) {
            double doubleVal = ((Double) attribValue).doubleValue();
            if (Double.isNaN(doubleVal) || Double.isInfinite(doubleVal)) {
                countNaN++; // increment the NaN count, but don't store NaN as the max
                return;
            }
        }

        Comparable curvalue = (Comparable) attribValue;

        if (!visited || compare(curvalue)) {
            maxvalue = curvalue;
            visited = true;
        }

        // throw new IllegalStateException("Expression is not comparable!");
    }

    @SuppressWarnings("unchecked")
    private boolean compare(Comparable curvalue) {
        if (maxvalue == null) {
            throw new IllegalStateException("maxvalue shouldn't be null when visited = true");
        }
        return curvalue.compareTo(maxvalue) > 0;
    }

    /**
     * Get the max value.
     *
     * @return Max value
     */
    public Comparable getMax() {
        if (!visited) {
            throw new IllegalStateException("Must visit before max value is ready!");
        }

        return maxvalue;
    }

    /** @return the number of features which returned a NaN */
    public int getNaNCount() {
        return countNaN;
    }

    /** @return the number of features which returned a null */
    public int getNullCount() {
        return countNull;
    }

    public void reset() {
        /** Reset the count and current maximum */
        this.visited = false;
        this.maxvalue = null;
        this.countNaN = 0;
        this.countNull = 0;
    }

    public Expression getExpression() {
        return expr;
    }

    @Override
    public CalcResult getResult() {
        if (!visited) {
            return CalcResult.NULL_RESULT;
        }

        return new MaxResult(maxvalue);
    }

    /**
     * Overwrites the result stored by the visitor. This should only be used by optimizations which
     * will tell the visitor the answer rather than visiting all features.
     *
     * <p>For 'max', the value stored is of type 'Comparable'.
     */
    public void setValue(Object result) {
        visited = true;
        maxvalue = (Comparable) result;
    }

    public static class MaxResult extends AbstractCalcResult {
        private Comparable maxValue;

        public MaxResult(Comparable newMaxValue) {
            maxValue = newMaxValue;
        }

        @Override
        public Object getValue() {
            Comparable max = maxValue;

            return max;
        }

        @Override
        public boolean isCompatible(CalcResult targetResults) {
            // list each calculation result which can merge with this type of result
            if (targetResults instanceof MaxResult || targetResults == CalcResult.NULL_RESULT) {
                return true;
            }

            return false;
        }

        @Override
        @SuppressWarnings("unchecked")
        public CalcResult merge(CalcResult resultsToAdd) {
            if (!isCompatible(resultsToAdd)) {
                throw new IllegalArgumentException("Parameter is not a compatible type");
            }

            if (resultsToAdd == CalcResult.NULL_RESULT) {
                return this;
            }

            if (resultsToAdd instanceof MaxResult) {
                // take the smaller of the 2 values
                Comparable toAdd = (Comparable) resultsToAdd.getValue();
                Comparable newMax = maxValue;

                if (newMax.getClass()
                        != toAdd.getClass()) { // 2 different data types, therefore convert
                    Class bestClass = CalcUtil.bestClass(toAdd, newMax);
                    if (bestClass != toAdd.getClass())
                        toAdd = (Comparable) CalcUtil.convert(toAdd, bestClass);
                    if (bestClass != newMax.getClass())
                        newMax = (Comparable) CalcUtil.convert(newMax, bestClass);
                }
                if (newMax.compareTo(toAdd) < 0) {
                    newMax = toAdd;
                }

                return new MaxResult(newMax);
            } else {
                throw new IllegalArgumentException(
                        "The CalcResults claim to be compatible, but the appropriate merge method has not been implemented.");
            }
        }
    }
}
